/*
 * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.huawei.cloudphone.virtualdevice.camera;

import static android.hardware.Camera.Parameters.FOCUS_MODE_AUTO;
import static android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE;
import static android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO;
import static android.hardware.Camera.Parameters.FOCUS_MODE_FIXED;
import static android.hardware.Camera.Parameters.FOCUS_MODE_INFINITY;
import static android.hardware.Camera.Parameters.FOCUS_MODE_MACRO;

import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.opengl.GLES11Ext;
import android.util.Log;
import android.view.SurfaceHolder;

import com.huawei.cloudphone.virtualdevice.common.IVirtualDeviceDataListener;
import com.huawei.cloudphone.virtualdevice.common.ParamBundle;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

public class VirtualCamera implements Camera.PreviewCallback {

    private static final String TAG = "VirtualCamera";
    private Camera.Parameters mParameters = null;
    private int mCameraId = 0;
    private Camera mCamera = null;
    private static final int LOG_LENGTH_LIMIT = 2000;
    private static final int SIZE_WIDTH_LIMIT = 1280;
    private static final int DEFAULT_WIDTH = 640;
    private static final int DEFAULT_HEIGHT = 480;

    private int mWidth = DEFAULT_WIDTH;
    private int mHeight = DEFAULT_HEIGHT;
    private int mFps = 15;
    private int mBitrate = 2000000;
    private SurfaceHolder mSurfaceHolder;
    private boolean mIsUseH264;

    private Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
    private IVirtualDeviceDataListener mCameraListener = null;
    private byte[] mPreviewBuffer = null;
    private SurfaceTexture mSurfaceTexture = null;
    private byte[] mH264Buffer;
    private AvcEncoder mAvcCodec = null;

    public int open(int cameraId) {
        mPreviewBuffer = null;
        mSurfaceTexture = null;
        mCameraId = cameraId;
        Camera.getCameraInfo(mCameraId, cameraInfo);
        try {
            if (mCamera != null) {
                mCamera.stopPreview();
                mCamera.setPreviewCallback(null);
                mCamera.release();
                mCamera = null;
            }
            Camera camera = Camera.open(mCameraId);
            Camera.Parameters param = camera.getParameters();
            mParameters = setDefaultParameters(param);
            camera.release();
            mIsUseH264 = AvcEncoder.isSupportH264();
        } catch (Exception e) {
            Log.e(TAG, "open camera failed, ", e);
            return -1;
        }
        return 0;
    }

    public int startPreview() {
        mCamera = Camera.open(mCameraId);
        Camera.Parameters param = setDefaultParameters(mCamera.getParameters());
        if (mIsUseH264) {
            param.setPreviewFormat(AvcEncoder.getImageFormat());
        } else {
            param.setPreviewFormat(ImageFormat.NV21);
        }

        Camera.Size size = param.getPreviewSize();
        if (mWidth <= 0 || mHeight <= 0 || mFps <= 0) {
            param.setPreviewSize(size.width, size.height);
        } else {
            param.setPreviewSize(mWidth, mHeight);
        }

        List<int[]> mFpsRangeList = new LinkedList<int[]>();
        mFpsRangeList = param.getSupportedPreviewFpsRange();
        for (int[] ele : mFpsRangeList) {
            Log.i(TAG, "startPreview: support fps range is " + ele[0] + "-" + ele[1]);
            if ((mFps * 1000 >= ele[0]) && (mFps * 1000 >= ele[1])) {
                param.setPreviewFpsRange(ele[0], ele[1]);
                param.setPreviewFrameRate(mFps);
            }
        }
        mCamera.setParameters(param);

        try {
            mSurfaceHolder = ParamBundle.getSurfaceHolder();
            if (mSurfaceHolder != null) {
                mCamera.setPreviewCallback(this);
                mCamera.setPreviewDisplay(mSurfaceHolder);
                mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
            } else {
                mSurfaceTexture = new SurfaceTexture(GLES11Ext.GL_TEXTURE_BINDING_EXTERNAL_OES);
                mCamera.setPreviewTexture(mSurfaceTexture);
            }
        } catch (IOException e) {
            Log.e(TAG, "startPreview: failed to set preview params", e);
            return -1;
        }

        int buffSize = mWidth * mHeight * ImageFormat.getBitsPerPixel(ImageFormat.NV21) / 8;
        mPreviewBuffer = new byte[buffSize];
        mCamera.addCallbackBuffer(mPreviewBuffer);
        mCamera.setPreviewCallbackWithBuffer(this);

        if (mIsUseH264) {
            mAvcCodec = new AvcEncoder(mWidth, mHeight, param.getPreviewFrameRate(), mBitrate
                    , new H264DataHandler());
            mH264Buffer = new byte[mWidth * mHeight * 3 / 2];
        }
        mCamera.startPreview();
        return 0;
    }

    public void stopPreview() {
        if (mCamera != null) {
            mCamera.stopPreview();
            mCamera.setPreviewCallback(null);
            mCamera.release();
            mCamera = null;
        }
        if (mAvcCodec != null) {
            mAvcCodec.stopEncoder();
            mAvcCodec = null;
        }
    }

    public void setOnRecvData(IVirtualDeviceDataListener listener) {
        mCameraListener = listener;
    }

    public void setResolution(int width, int height) {
        mWidth = width == -1 ? DEFAULT_WIDTH : width;
        mHeight = height == -1 ? DEFAULT_HEIGHT : height;
    }

    public int getWidth() {
        return mWidth;
    }

    public int getHeight() {
        return mHeight;
    }

    public int getFacing() {
        return cameraInfo.facing;
    }

    public int getOrientation() {
        return cameraInfo.orientation;
    }

    public byte[] getParameters() {
       return mParameters.flatten().getBytes();
    }

    public void setParameters(byte[] data) {
        if (mParameters == null) {
            return;
        }
        mParameters.unflatten(new String(data));
    }

    public void setFps(int fps) {
        mFps = fps;
    }

    public boolean isSupportH264() {
        return AvcEncoder.isSupportH264();
    }

    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        Camera.Size size = camera.getParameters().getPreviewSize();
        if (mIsUseH264) compressWithH264(data);
        if (mSurfaceTexture != null) {
            camera.addCallbackBuffer(mPreviewBuffer);
        }
    }

    private void compressWithH264(byte[] data) {
        mAvcCodec.offerEncoder(data, mH264Buffer);
    }

    private Camera.Parameters setDefaultParameters(Camera.Parameters param) {
        Set<String> supportFocusModes = new HashSet<>(Arrays.asList(FOCUS_MODE_AUTO
                , FOCUS_MODE_CONTINUOUS_PICTURE, FOCUS_MODE_CONTINUOUS_VIDEO, FOCUS_MODE_FIXED
                , FOCUS_MODE_INFINITY, FOCUS_MODE_MACRO));
        List<String> focusModes = param.getSupportedFocusModes();

        StringBuilder sb = new StringBuilder();
        for (String str : focusModes) {
            if (supportFocusModes.contains(str)) {
                sb.append(str + ",");
            }
        }
        sb.deleteCharAt(sb.length() - 1);
        param.set("focus-mode-values", sb.toString());
        if (sb.toString().contains(FOCUS_MODE_CONTINUOUS_PICTURE)) {
            param.setFocusMode(FOCUS_MODE_CONTINUOUS_PICTURE);
        }

        List<Camera.Size> picSize = param.getSupportedPictureSizes();
        sb = new StringBuilder();
        for (Camera.Size size : picSize) {
            if (size.width < SIZE_WIDTH_LIMIT) {
                sb.append(size.width + "x" + size.height + ",");
            }
        }
        sb.deleteCharAt(sb.length() - 1);
        param.set("picture-size-values", sb.toString());

        List<Camera.Size> previewSize = param.getSupportedPreviewSizes();
        sb = new StringBuilder();
        for (Camera.Size size : previewSize) {
            if (size.width < SIZE_WIDTH_LIMIT) {
                sb.append(size.width + "x" + size.height + ",");
            }
        }
        sb.deleteCharAt(sb.length() - 1);
        param.set("preview-size-values", sb.toString());

        List<Camera.Size> videoSize = param.getSupportedVideoSizes();
        sb = new StringBuilder();
        for (Camera.Size size : videoSize) {
            if (size.width < SIZE_WIDTH_LIMIT) {
                sb.append(size.width + "x" + size.height + ",");
            }
        }
        sb.deleteCharAt(sb.length() - 1);
        param.set("video-size-values", sb.toString());
        return param;
    }

    public void generateKeyFrame() {
        if (mAvcCodec != null) {
            mAvcCodec.generateKeyFrame();
        }
    }

    class H264DataHandler implements AvcEncoderDataHandler {
        @Override
        public void handleData(byte[] data, int length) {
            if (mCameraListener != null) mCameraListener.onRecvData(data, length, mCameraId);
        }
    }
}
